記憶體資料庫最怕的就是突然停電,辛辛苦苦存的資料瞬間灰飛煙滅。為了解決這問題,Redis 提供了 AOF (Append-Only File) 日誌與 RDB (Redis Database) 快照兩種方案。
今天先實作 AOF 的基礎寫入,至少讓資料不只存在記憶體裡,重啟時還有機會救回來。
AOF 說穿了就是記帳:每次成功執行寫命令(像 SET、DEL、LPUSH),就把那條命令用 RESP 格式追加到日誌檔尾端。
伺服器重啟時,再從頭到尾 replay 這些寫命令,把記憶體狀態重建回來。
客戶端寫請求 ----> [執行命令] ----> 寫入記憶體 DB
|
v
[追加 AOF] ----> 寫入 appendonly.aof 檔案
在傳統的關係型資料庫(如 MySQL, PostgreSQL)中,通常使用 WAL (預寫式日誌) 機制——先寫日誌,再寫資料庫記憶體。這是為了保證崩潰恢復(Crash Recovery)的安全。
而 Redis 採用了相反的順序:先執行命令修改記憶體,再寫入 AOF 日誌。
這樣做有幾個好處:
我在 code/db/aof.go 中實現了 AofLogger。
type AofLogger struct {
mu sync.Mutex
file *os.File
filename string
dbEngine *DB
stopSync chan struct{}
}
func NewAofLogger(filename string, dbEngine *DB) (*AofLogger, error) {
// 以 O_APPEND 追加模式開啟檔案,若不存在則建立
file, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
if err != nil {
return nil, err
}
// ... 初始化結構 ...
}
每當dispatcher 執行寫命令成功後,會呼叫此方法將其序列化並寫入檔案:
func (a *AofLogger) Write(val resp.Value) error {
a.mu.Lock()
defer a.mu.Unlock()
bytes := val.Marshal() // 直接將命令的 RESP 表示寫入檔案
_, err := a.file.Write(bytes)
return err
}
寫這個 AOF 載入時,我差點把游標搞丟,忘記把 a.file.Seek(0, io.SeekEnd) 加在最後,結果新命令直接覆蓋掉原本的檔案內容,找了半天才抓到這個雷。
當伺服器啟動時,我在 Start() 中調用 LoadAndReplay,利用之前實作的 resp.Reader 逐個讀取 RESP 命令並重新分發執行:
func (a *AofLogger) LoadAndReplay(replayFunc func(val resp.Value)) error {
a.mu.Lock()
defer a.mu.Unlock()
// 定位到檔案開頭
_, err := a.file.Seek(0, io.SeekStart)
if err != nil { return err }
reader := resp.NewReader(a.file)
for {
val, err := reader.ReadValue()
if err != nil {
if errors.Is(err, io.EOF) { break } // 讀到檔案結尾,正常退出
return err
}
replayFunc(val) // 呼叫重播回呼,在記憶體中重跑命令
}
// 重播完成後,指針定位回結尾以利追加
_, err = a.file.Seek(0, io.SeekEnd)
return err
}
先把 Server 跑起來(記得開啟 AOF 設定):
go run ./code/main.go
我們開另一個終端機,隨便寫入幾筆資料:
printf "*3\r\n\$3\r\nSET\r\n\$4\r\nname\r\n\$5\r\nalice\r\n" | nc localhost 6379
printf "*3\r\n\$3\r\nSET\r\n\$3\r\nage\r\n\$2\r\n18\r\n" | nc localhost 6379
然後直接去檢查專案目錄底下長出來的 appendonly.aof 檔案:
cat appendonly.aof
# 預期輸出(RESP 格式的指令日誌):
# *3
# $3
# SET
# $4
# name
# $5
# alice
# *3
# $3
# SET
# $3
# age
# $2
# 18
今天把 AOF 的基本追加和啟動 replay 先弄起來。實作這個的時候一開始用字串拼接硬幹,結果壓測直接 OOM 被系統砍掉,改成 bufio.Writer 配 buffer 之後才算勉強撐住。
不過現在每次寫指令都會碰磁碟,效能肯定不好看。明天來補每秒非同步 fsync,看看 I/O 能不能救回來一點。